Technical Note TN2037
Exclusive File Access in Mac OS X

目次

このテクニカルノートでは、現バージョンの Mac OS X での排他的ファイルアクセス権の取得にまつわる問題、および従来の Mac OS との相違点を説明します。排他的ファイルアクセスは、すべての Mac OS X デベロッパ、Carbon、Cocoa、Java、および BSD に影響する問題です。

[2002 年 5 月 1 日]






概要

従来の Mac OS (Mac OS X より以前)で fsWrPermfsRdWrPerm、またはデフォルトの fsCurPerm を指定してファイルを開くと、書き込みアクセス権を使ってこのファイルを開こうとしている他のすべてのアプリケーションは、ファイルを開けませんでした。通常、書き込みアクセス権の場合、ファイルを開こうとすると fsRdWrPerm エラーが戻されましたが、読み取り専用アクセス権の場合はファイルを開けます。このようなデフォルトの動作によって、ファイルに対して、1 人の「書き手」と複数の「読み手」が可能となります。

Mac OS X の BSD サブシステムは、従来の Mac OS と同じ方法では、ファイルの読み書きの権限を適用しません。書き込みのためにファイルを開いたときに、他のプロセスが同じファイルに書き込めないことは保証されません。BSD のデフォルトの動作では、単一のファイルに対して複数の「書き手」が許可されます。現在の実装では、Mac OS X の File Manager 関連の関数はすべて、基盤の BSD ファイルシステムを呼び出すようになっています。このため、ローカルボリュームで、PBHOpenDFPBHOpenRFPBHOpenPBOpenForkFSOpenForkHOpen などを使ってファイルを開き、パーミッションの値として fsCurPermfsWrPerm、または fsRdWrPerm を渡しても、Mac OS X では排他的ファイルアクセスは保証されません。Mac OS X では、すでに開かれているファイルに対して書き込みパーミッションを指定して Open を呼び出しても、エラーに遭遇することなく成功します。同様に PBLockRange() ルーチンでも、バイト範囲が他のプロセスによって変更できないことが、実際には保証されないことがあります。これらのルーチンはエラーなしで戻るため、基盤のファイルアクセスについて何らかの想定をする前に、あらかじめ排他的ファイルアクセスが利用可能かどうかを調べてください。「アドバイザリロックをサポートする」機能がない場合、アプリケーションでは、ファイルが他のアプリケーションによってすでに使用されているかどうかを判別できません。

Mac OS X の AppleShare サーバ および Personal File Sharing は、ネットワークを介してアクセスされるボリュームに対して排他的ファイルアクセスと範囲のロックを適用します。しかし、この機能は、ネットワーク化されたファイル共有接続をまたいでファイルにアクセスする場合のみ利用でき、サーバ自体で稼動しているアプリケーションには利用できません。

非排他環境で作業をするためのガイドライン

複数のアプリケーションが同じファイルに書き込むのを防ぐために(または、バイト範囲ロックを通じて書き込みアクセス権を制御するために)、多くのアプリケーションが従来の Mac OS の File Manager に依存していることをアップルは認識しています。このような動作は、どのバージョンの Mac OS X にも実装されていないため、お使いのコードで使用できる一般的な回避策を以下に示します。
DoS 攻撃によって、あるプロセスが、別のプロセスが必要とするファイルを排他ロック付きで開くことにより、他方のプロセスがブロックされてしまう、という事態を防ぐために、BSD は排他ロックを使わずに設計されました。



排他的ファイルアクセスが利用可能かどうかのチェック

Mac OS X は、BSD アドバイザリロックを、排他的であるかのように実行することで、排他的ファイルアクセス、すなわち、1 つのファイルに対して一人の「書き手」と多数の「読み手」をアプリケーションフレームワーク、Carbon、Cocoa、および Java を介して実行します。「アドバイザリロックをサポートする」機能は、対象ボリュームの OS とファイルシステムの両方がアドバイザリロックをサポートする場合は定義されます。この場合、アプリケーションフレームワークのデフォルトの動作では、書き込み可能として開かれた場合、排他的アクセスを使ってファイルを開きます。これらのフレームワークで作成されたアプリケーションは、自動的にこの機能を利用できるため、変更を加える必要はありません。排他的ファイルアクセスをサポートする条件を満たしている場合、PBLockRange も BSD アドバイザリロックを呼び出します。この時点で、PBLockRange は BSD アドバイザリロックに基づくため、ローカルファイルだけでなく、ファイルサーバのファイルにも範囲ロックを適用できます。

Mac OS X のすべてのバージョンの Carbon が排他的ファイルアクセスをサポートしているわけではなく、すべてのファイルシステムが BSD アドバイザリロックをサポートしているわけでもないため、基盤のファイルアクセスの動作について何らかの想定をする前に、あらかじめ 2 つのことをチェックする必要があります。これらの機能は、gestalt ビットである gestaltFSSupportsExclusiveLocks と、GetVolParms ビットである bSupportsExclusiveLocks の両方がセットされている場合にだけ利用できると想定できます。たとえば、Carbon Framework File Manager ルーチンは SupportsExclusiveFileAccess が true を戻した場合にデフォルトでアドバイザリロックをサポートします。



#ifndef gestaltFSSupportsExclusiveLocks
   #define    gestaltFSSupportsExclusiveLocks    15
   #define    bSupportsExclusiveLocks            18
#endif

Boolean    SupportsExclusiveFileAccess( short vRefNum )
{
   OSErr                    err;
   GetVolParmsInfoBuffer    volParmsBuffer;
   HParamBlockRec           hPB;
   long                     response;
   Boolean                  exclusiveAccess    = false;

   err = Gestalt( gestaltSystemVersion, &response );
   if ( (err == noErr) && (response < 0x01000) )
   {
      err = Gestalt( gestaltMacOSCompatibilityBoxAttr, &response );
      if ( (err != noErr)
      || ((response & (1 << gestaltMacOSCompatibilityBoxPresent)) == 0) )
         return( true );        //    Classic ではなく、Mac OS 9 で稼動
   }

   err = Gestalt( gestaltFSAttr, &response );
   if ( (err == noErr)
        && (response & (1L << gestaltFSSupportsExclusiveLocks)) )
   {
      hPB.ioParam.ioVRefNum     = vRefNum;
      hPB.ioParam.ioNamePtr     = NULL;
      hPB.ioParam.ioBuffer      = (Ptr) &volParmsBuffer;
      hPB.ioParam.ioReqCount    = sizeof( volParmsBuffer );
      err = PBHGetVolParmsSync( &hPB );
      if ( err == noErr )
         exclusiveAccess =
                (volParmsBuffer.vMExtendedAttributes
                & (1L << bSupportsExclusiveLocks));
   }

   return( exclusiveAccess );
}


ボリュームが、PBLockRange によるバイト範囲ロックをサポートするかどうかをチェックするには、GetVolParms から戻される bHasOpenDeny ビットをチェックします。PBLockRange の詳細については、Technical Note FL37 を参照してください。



      hPB.ioParam.ioVRefNum     = vRefNum;
      hPB.ioParam.ioNamePtr     = NULL;
      hPB.ioParam.ioBuffer      = (Ptr) &volParmsBuffer;
      hPB.ioParam.ioReqCount    = sizeof( volParmsBuffer );
      err = PBHGetVolParmsSync( &hPB );
      if ( err == noErr )
         supportsByteRangeLocking =
             (volParmsBuffer.vMAttrib & (1L << bHasOpenDeny));

先頭に戻る



一般的な回避策

次の 2 つのテクニックは、排他的ファイルアクセスを適用しないプラットフォームでこの問題を回避するためによく使われます。



ロックファイル

よく使われる一般的なアプローチは、開くファイルのあるディレクトリと同じディレクトリに「ロックファイル」を作成することです。たとえば、書き込みアクセス権を使って "foo" ファイルを開くときに必ず、最初に "foo.lock" というロックファイルを同じ場所に作成します。ファイルがすでに存在していて、ファイルの作成に失敗した場合、"foo" はすでに別のアプリケーションによって開かれていると想定できます。"foo" を閉じたら、アプリケーションは "foo.lock" も削除しなければなりません。このテクニックの長所は、基盤のファイルシステムについて、「ファイル作成処理はアトミックである」という想定しかしないことです。はっきりしている短所は、OS がこの方法をサポートしていないため、各アプリケーションが独自にロックファイルメカニズムを実装しなければならないことと、ロックファイルの命名に関して、一致した標準または規則がないことです。

サンプル GrabBag は、この「ロックファイル」テクニックのバリエーションを実装しています。ロックファイルを作成するだけでなく、ファイルに ProcessSerialNumber (PSN) を格納します。このコードは、ファイルを開く前に、ロックファイルがあるかどうかをチェックし、あれば、ファイルの PSN が有効かどうかを調べます。これは、アプリケーションがクラッシュしたときに、ファイルがロックされたままの状態になるのを防ぐのに役立ちます。



重要:
回避策は、「アドバイザリロックをサポートする」機能をサポートするシステムでは、再評価しなければなりません。この機能は、利用できるようになったときに発表します。



コピーの編集

もう 1 つの回避策は、対象ファイルの一意のコピーを操作するという方法です。編集のためにファイルが開かれると、そのファイルのコピーが /tmp ディレクトリに一意の名前で作成され、開かれます。ユーザがドキュメントを保存しようとすると、ファイルを開く時にキャッシュされた日付とオリジナルの最終変更日が照合されます。日付が変わっていれば、ファイルが変更されていることが分かります。

先頭に戻る



Mac OS X のソリューション



BSD アドバイザリロック

Mac OS X の BSD サブシステムは、排他的書き込みアクセス権(強制ロックなど)のための仕組みを実装していませんが、アドバイザリロックは提供しています。アドバイザリロックは、リンクされたレコードロックのリストが基盤のファイルシステムによって保持される、参加型のロック機構です。アプリケーションや他のアプリケーションがロックを尊守している限り、1 度に 1 つのアプリケーションだけが、特定のファイルへの書き込みアクセス権を取得します。これらのロックは参加型のものなので、アドバイザリロックの尊守または無視は、アプリケーション開発者の選択および責任になります。アドバイザリロックを使用する場合は、後述の手順に従ってください。アプリケーションフレームワーク(Carbon、Cocoa、Java)のアドバイザリロック機能をサポートしているバージョンの OS で、そのフレームワークを通じてファイルにアクセスすると、フレームワークのファイルアクセスメソッドを使用すれば、アドバイザリロックは自動的に提供されます。



重要:
すべてのアプリケーションが、アドバイザリロックを尊守し、使用するべきです。



BSD のファイル I/O 関連関数を直接呼び出すアプリケーションは、この動作を自動的に利用することができません。そのため、ファイルを開くときに適切なフラグを指定することにより、アドバイザリロックを設定し、尊守するように変更する必要があります。



たとえば、次の呼び出しを、

    fd = open( "./foo", O_RDWR );

次のように変更することを検討する必要があります。

    fd = open( "./foo", O_RDWR + O_EXLOCK + O_NONBLOCK );

ここでは、O_EXLOCK は、アトミックに排他ロックを取得することを意味し、O_NONBLOCK は、オープン時またはデータが利用できるようになるまでブロックしない、またはデバイスあるいはファイルが準備完了になるまで、または利用できるようになるまで待たないことを意味します。



先頭に戻る



アドバイザリロックの実装

書き込みアクセス権を指定して、open(2) の System.framework バージョンを呼び出す場所はすべて、これまで正しく動作していたとしても、パラメータを変更し、"O_EXLOCK + O_NONBLOCK" フラグを追加し、戻されるエラーを処理するようにしなければなりません。他のプロセスによって、ファイルが排他的アクセスを指定されて開かれていると、open(2) の呼び出しは失敗します。

アドバイザリロックは、プロセスとファイルに関連付けられています。これには、次の 2 つの意味があります。

  • プロセスが終了すると、そのすべてのロックが解放される。

  • 記述子が閉じられると、その記述子によって参照されていたファイルのロックがすべて解放される。



バイト範囲ロックの実装

BSD は、fcntl() 関数通じてバイト範囲のアドバイザリロックもサポートしています。アドバイザリロックを使うと、アプリケーションは、将来、Carbon、Classic、およびおよび他のアプリケーションと協調して動作できるようになります。このような場合、O_EXLOCK をセットしてファイルを開き、fcntl() を呼び出して範囲をロックします。

Stevens の「Advanced Programming in the Unix Environment」(Stevens、1999 年、367 ページ)に、UNIX サービスの fcntl() を使って、ファイルの一部を読み書き用にロックするためのテクニックがいくつか説明されています。



警告:
ブロックされているファイルロックリクエストは、シグナルによって割り込むことができます。その場合、ロック処理は EINTR を戻します。このため、実際にはロックを取得できていないのに、取得できたと思ってしまうことがあります。これを解決するには、ロック時にシグナルをブロックします。または、ロック処理によって戻された値をテストし、その値が EINTR の場合は、再度ロックするという解決策もあります。または、ここで採用する、特に何もしないという解決策もあります。



レコードロック とは、最初のプロセスがファイルの一部を読み込んでいる、または変更しているときに、別のプロセスがそのファイルの同じ領域を変更するのを防ぐプロセスの機能を説明するために使われる用語です。BSD では、fcntl 関数を通じてレコードロックメカニズムにアクセスできます。



    #include <sys/types.h>
    #include <unistd.h>
    #include <fcntl.h>

    /*
     * 戻り値:
     *    エラーの場合は -1
     */
    int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */ );


以下に、flock 構造体を指す 3 番目の引数(flockptr)から説明します。



struct flock {
    short l_type;     /* F_RDLCK (共有読み取りロック)または
                       * F_WRLCK (共有書き込みロック)または
                       * F_UNLCK (リージョンのロック解除)
                       */
    off_t l_start;    /* l_whence からのオフセット(バイト単位) */
    short l_whence;   /* SEEK_SET: ファイルのオフセットを、ファイルの
                       *           先頭から l_start バイトに設定
                       * SEEK_CUR: ファイルのオフセットを、現在の値と
                       *           l_start の合計に設定(「+」または
                       *           「-」が可能)
                       * SEEK_END: ファイルのオフセットを、ファイルの
                       *           サイズとl_start の合計に設定(「+」
                       *           または「-」が可能)
                       */
    off_t l_len;      /* リージョンの長さ(バイト単位)
                       * 特殊ケース:(l_len == 0) の場合、ファイルの最大
                       * オフセットまでロックを拡張。これによりファイルの
                       * 任意の場所を起点に、ファイルに追加されるすべての
                       * データを含むリージョンをロックできる
                       */
    pid_t l_pid;      /* cmd = F_GETLK の場合に戻される */
}


この構造体は以下のことを表しています。

  • 要求するロックのタイプ(読み取りロック、書き込みロック、ロック解除など)

  • ロックまたはロック解除されているリージョンのバイトオフセットの開始(l_start および l_whence

  • リージョンのサイズ(l_len

ファイル全体をロックするには、l_start および l_whence を、ファイルの先頭を指すように設定し(l_start= 0l_whence= SEEK_SET など)、 長さ(l_len)に 0 を指定します。

あるバイトに共有読み取りロックを設定できるプロセスの数には制限はありませんが、排他的書き込みロックは、1 つのプロセスしか設定できません。読み取りロックを取得するには、読み取りのために記述子を開く必要があり、リージョンに排他的書き込みロックが設定されていてはなりません。書き込みロックを取得するには、記述子を書き込みのために開く必要があり、リージョンには排他的書き込みロックも読み取りロックも設定されていてはなりません。

次に、fcntl の 2 番目のパラメータ(cmd)について説明します。可能なコマンドおよびその意味を次の表に示します。



コマンド

意味

F_GETLK flockptr で記述されているロックが、他の何らかのロックでブロックされているかどうかを判別します。ロックの作成を妨げる別のロックが存在する場合、既存のロックの情報が、flockptr で示されている情報を上書きします。ロックの作成を妨げるロックが存在しない場合、flockptr が指す構造体は、l_type メンバが F_UNLCK に設定される点を除けばそのままです。
F_SETLK flockptr によって記述されているロックを設定します。別のロックによってすでにリージョンがロックされていて、ロックを取得できない場合、fcntl-1 を戻し、errnoEACCES または EAGAIN のどちらかに設定されます。
F_SETLKW このコマンドは、F_SETLK のブロックバージョンです(コマンドの「W」は "wait" の意味)。現在、別のプロセスによって、要求したリージョンの一部がロックされているために、要求した読み取りロックまたは書き込みロックが得られない場合、呼び出し側のプロセスをスリープ状態にします。シグナルをキャッチすると、スリープ状態に割り込みがかかります。


F_GETLK を使ってロックをテストしてから、F_SETLK または F_SETLKW を使ってそのロックを取得しようとするのは、アトミックな処理ではありません。2 つの fcntrl の呼び出しの間に、別のプロセスが割り込んで同じロックを取得する可能性がないという保証はありません。

毎回 flock 構造体を割り当てて、すべての要素を埋める手間を省くために、Stevens は、lock_reg 関数と、それを呼び出すいくつかのマクロを定義しています。このマクロによってパラメータの数が 2 つ減り、上記の F_* 定数を覚えなくても済むことに注目してください。



#define read_lock(fd, offset, whence, len)    ¥
         lock_reg  (fd, F_SETLK,  F_RDLCK, offset, whence, len)

#define readw_lock(fd, offset, whence, len)   ¥
         lock_reg  (fd, F_SETLKW, F_RDLCK, offset, whence, len)

#define write_lock(fd, offset, whence, len)   ¥
         lock_reg  (fd, F_SETLK,  F_WRLCK, offset, whence, len)

#define writew_lock(fd, offset, whence, len)  ¥
         lock_reg  (fd, F_SETLKW, F_WRLCK, offset, whence, len)

#define un_lock(fd, offset, whence, len)      ¥
         lock_reg  (fd, F_SETLK,  F_UNLCK, offset, whence, len)


pid_t    lock_test(int, int , off_t , int , off_t );

#define    is_readlock(fd, offset, whence, len) ¥
            lock_test(fd, F_RDLCK, offset, whence, len)
#define    is_writelock(fd, offset, whence, len) ¥
            lock_test(fd, F_WRLCK, offset, whence, len)


int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;

    lock.l_type   = type;     /* F_RDLCK、F_WRLCK、F_UNLCK */
    lock.l_start  = offset;   /* l_whence からのオフセット(バイト単位) */
    lock.l_whence = whence;   /* SEEK_SET、SEEK_CUR、SEEK_END      */
    lock.l_len    = len;      /* バイト数(0 は EOF) */

    return ( fcntl(fd, cmd, &lock) );
}


pid_t    lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
  struct flock lock;
  lock.l_type = type;     /* F_RDLCK または F_WRLCK */
  lock.l_start = offset;  /* l_whence からのオフセット(バイト単位) */
  lock.l_whence = whence; /* SEEK_SET、SEEK_CUR、SEEK_END */
  lock.l_len = len;       /* バイト数(0 は EOF)*/
  if (fcntl(fd,F_GETLK,&lock) < 0){
    perror("fcntl"); exit(1);}
  if (lock.l_type == F_UNLCK)
    return (0);        /* false、リージョンは別のプロセスにロックされていない */
  return (lock.l_pid); /* true、ロック所有者の PID を戻す */
}


自動継承とレコードロックの解放について、次の 3 つの重要な規則があります。

  • ロックはプロセスとファイルに関連付けられています。プロセスが終了すると、すべてのロックが解除されます。記述子を閉じると、そのプロセスの記述子によって参照されるファイルのロックはすべて解除されます。

  • fork された子プロセスがロックを継承することはありません(さもなければ、書き込みロックを共有する 2 つのプロセスができてしまいます)。

  • exec された新しいプログラムはロックを継承することができます。これは、BSD では必須ではないため、マシンに依存しています。

先頭に戻る



参考文献

Stevens、Richard W. 著『Advanced Programming in the Unix Environment』
Addison Wesley Longman, Inc. 刊(1999年)
ISBN: 0201563177

先頭に戻る



ダウンロード

Acrobat gif

このテクニカルノートの PDF 版

ダウンロード

Redbook gif

GrabBag――「ロックファイル」の使用方法を示す Carbon アプリケーション(200K)

ダウンロード


先頭に戻る